/**
 * 
 */
package optimization.naturalOptimization;

import java.util.ArrayList;
import java.util.Observable;

import observer.ConsoleObserver;
import optimization.IdentificationI;
import optimization.OptimizationI;
import optimization.naturalOptimization.evolutionaryAlgorithm.EvolutionaryAlgorithm;
import optimization.naturalOptimization.fitness.CircleBoundCheck;
import optimization.naturalOptimization.fitness.Rastrigin;
import optimization.naturalOptimization.fitness.Rosenbrock;
import optimization.naturalOptimization.fitness.Schwefel;
import optimization.naturalOptimization.fitness.SimpleFitnessFunction;
import optimization.naturalOptimization.fitness.Sphere;
import optimization.naturalOptimization.particleSwarmOptimization.ParticleSwarmOptimization;
import optimization.naturalOptimization.searchSpace.DoubleArraySearchSpace;
import optimization.naturalOptimization.searchSpace.SearchSpace;
import optimization.naturalOptimization.terminationCriterion.CalculationTimeTermination;
import optimization.naturalOptimization.terminationCriterion.FitnessTermination;
import optimization.naturalOptimization.terminationCriterion.GenerationTermination;
import optimization.naturalOptimization.terminationCriterion.NoChangeTerminationCriterion;
import optimization.naturalOptimization.terminationCriterion.StandardDeviationTermination;
import optimization.naturalOptimization.terminationCriterion.TerminationCriterion;
import optimization.randomNumberGenerator.JavaRandomNumberGenerator;
import optimization.randomNumberGenerator.MersenneTwisterFast;
import optimization.randomNumberGenerator.RandomNumberGenerator;
import properties.JOFProperties;
import exceptions.NoInitializationException;
import export.CSVExport;
import export.ErrorLog;
import export.IndividualDataExport;
import export.TEXExport;
import export.TXTExport;
import export.XLSExport;

/**
 * This class provides the super class for natural optimization algorithms. In
 * addition, the main flow is controlled and general parameters are managed.
 * 
 * @author Kevin Wagner
 * @version 1.0
 */
public abstract class NaturalOptimization extends Observable implements
		OptimizationI<ArrayList<double[]>>, IdentificationI {

	// Statusdefinitionen
	/**
	 * Status value for not initialized and not running
	 */
	public final static int notInizialized = 0;
	/**
	 * Status value for initialized
	 */
	public final static int inizialized = 1;
	/**
	 * Status value for running
	 */
	public final static int running = 2;

	private int status = 0; // 0: not ininitalized, not running; 1: initialized;
							// 2: running
	private JOFProperties properties = new JOFProperties();
	private ArrayList<double[]> result;
	private int currentGeneration = 0;
	@SuppressWarnings("rawtypes")
	private SearchSpace searchSpace = new DoubleArraySearchSpace(this);
	@SuppressWarnings("rawtypes")
	private SimpleFitnessFunction fitnessFunction = new Sphere();
	private boolean fitnessChanged = false;
	private RandomNumberGenerator rng = null;
	private ArrayList<TerminationCriterion> stopCriterions = new ArrayList<TerminationCriterion>();

	/*
	 * (non-Javadoc)
	 * 
	 * @see optimization.OptimizationI#getParameters()
	 */
	@Override
	public ArrayList<String> getParameters() {
		ArrayList<String> parameters = new ArrayList<String>();
		parameters.add("Termination Criterion");
		parameters.add("Fitness Function");
		parameters.add("Fitness Target");
		parameters.add("Search Space");
		parameters.add("Random Number Generator (RNG)");
		parameters.add("  Seed");
		return parameters;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see optimization.OptimizationI#getParameterConfiguration()
	 */
	@Override
	public ArrayList<String> getParameterConfiguration() {
		ArrayList<String> parameters = new ArrayList<String>();
		String crit = "";
		for (int i = 0; i < stopCriterions.size(); i++) {
			if (i == stopCriterions.size() - 1) {
				crit += stopCriterions.get(i).getIndentification();
			} else {
				crit += stopCriterions.get(i).getIndentification() + "\n";
			}
		}
		parameters.add(crit);
		parameters.add(getFitness().getIndentification());
		String temp = "";
		if (fitnessFunction.isMin()) {
			temp = "Minimum";
		} else {
			temp = "maximum";
		}
		parameters.add(temp);
		parameters.add(getSearchSpace().getIndentification());
		parameters.add(getRng().getIndentification());
		parameters.add(getRng().getSeed() + "");
		return parameters;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see optimization.OptimizationI#perform()
	 */
	@Override
	public boolean perform() {
		try {
			if (!isInitialized()) {
				throw new NoInitializationException();
			}
			fitnessChanged = false;
			pushMessage("Beginn Optimization");
			setStatus(NaturalOptimization.running);
			while (!stop()) {
				setCurrentGeneration(getCurrentGeneration() + 1);
				boolean status = performStep();
				if (!status) {
					break;
				}
				exportIndividualData();
			}
			setStatus(NaturalOptimization.notInizialized);
			int postCommaDigits = Integer.valueOf(properties.getProperty(
					"postCommaDigits", "2"));
			if (Boolean.valueOf(properties.getProperty("resultTXTExport",
					"true"))) {
				TXTExport.exportResult(result, postCommaDigits);
			}
			if (Boolean.valueOf(properties.getProperty("resultXLSExport",
					"true"))) {
				XLSExport.exportResult(result);
			}
			if (Boolean.valueOf(properties.getProperty("resultTEXExport",
					"true"))) {
				TEXExport.exportResult(result, postCommaDigits);
			}
			if (Boolean.valueOf(properties.getProperty("resultCSVExport",
					"true"))) {
				CSVExport.exportResult(result, postCommaDigits);
			}
			pushMessage("Optimization Finished");
		} catch (Exception e) {
			pushError(e);
			return false;
		}
		return true;
	}

	/**
	 * Tests the termination criteria.
	 * 
	 * @return {@code TRUE}, if optimization termination achieved.
	 */
	private boolean stop() {
		for (int i = 0; i < stopCriterions.size(); i++) {
			TerminationCriterion t = stopCriterions.get(i);
			if (t.isFinished()) {
				pushMessage(t.getIndentification() + " achieved target");
				return true;
			}

		}
		return false;
	}

	/**
	 * Performs one optimization step (one generation).
	 * 
	 * @return {@code TRUE}, if a abort criterion achieved.
	 */
	public abstract boolean performStep();

	/*
	 * (non-Javadoc)
	 * 
	 * @see optimization.OptimizationI#init()
	 */
	@Override
	public boolean init() {
		if (Boolean.valueOf(properties.getProperty("consoleOutput", "true"))) {
			this.addObserver(new ConsoleObserver());
		}
		if (Boolean.valueOf(getProperties().getProperty("generationLimit",
				"true"))) {
			stopCriterions.add(new GenerationTermination(this));
		}
		if (Boolean.valueOf(getProperties().getProperty("timeLimit", "true"))) {
			stopCriterions.add(new CalculationTimeTermination(this));
		}
		if (Boolean.valueOf(getProperties()
				.getProperty("noChangeLimit", "true"))) {
			stopCriterions.add(new NoChangeTerminationCriterion(this));
		}
		if (Boolean.valueOf(getProperties().getProperty("stdLimiter", "true"))) {
			stopCriterions.add(new StandardDeviationTermination(this));
		}
		if (Boolean.valueOf(getProperties().getProperty("fitnessLimiter",
				"true"))) {
			stopCriterions.add(new FitnessTermination(this));
		}
		String min = properties.getProperty("optTarget", "min");
		if (min.toLowerCase().equals("min")) {
			getFitness().setMin(true);
		} else {
			getFitness().setMin(false);
		}
		searchSpace.setDimensionality(getDimensionality());
		if (!getSearchSpace().checkBoundariesCompatibility()) {
			pushError(new Exception("Incompatible Boundaries in Search Space"));
		}
		if (stopCriterions.size() == 0) {
			pushError(new Exception("No Stop Criterion added"));
			return false;
		}
		if (!fitnessChanged) {
			String fit = properties.getProperty("fitness");
			if (fit.toLowerCase().contains("sphere")) {
				fitnessFunction = new Sphere();
			} else if (fit.toLowerCase().contains("rosenbrock")) {
				fitnessFunction = new Rosenbrock();
			} else if (fit.toLowerCase().contains("rastrigin")) {
				fitnessFunction = new Rastrigin();
			} else if(fit.toLowerCase().contains("schwefel")){
				fitnessFunction = new Schwefel();
			}else if(fit.toLowerCase().contains("circleboundcheck")){
				fitnessFunction = new CircleBoundCheck();
			}
		}
		setCurrentGeneration(0);
		setRng();
		result = new ArrayList<double[]>();
		String initMsg = "Initialization of a " + getIndentification() + "\n";
		initMsg += "Configuration:\n";
		for (int i = 0; i < getParameters().size(); i++) {
			initMsg += "  " + getParameters().get(i) + ": "
					+ getParameterConfiguration().get(i) + "\n";
		}
		pushMessage(initMsg);
		return true;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see optimization.OptimizationI#isInitialized()
	 */
	@Override
	public boolean isInitialized() {
		return status == 1;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see optimization.OptimizationI#isRunning()
	 */
	public boolean isRunning() {
		return status == 2;
	}

	/**
	 * Checks to see if changes are allowed.
	 * 
	 * @return {@code true}, if changes are allowed at this time.
	 */
	public boolean changesAllowed() {
		return status != 2;
	}

	// //////////////////////////////////////////////////////////////////////
	// //////////////////////////////////////////////////////////////////////
	// ///////////////////////Getter und Setter//////////////////////////////
	// //////////////////////////////////////////////////////////////////////
	// //////////////////////////////////////////////////////////////////////

	/**
	 * Sets the status of initialization of the optimization.
	 * 
	 * @param status
	 *            new status of initialization
	 */
	public void setStatus(int status) {
		this.status = status;
	}

	/**
	 * Returns the current generation of the optimization.
	 * 
	 * @return the current generation
	 */
	public int getCurrentGeneration() {
		return currentGeneration;
	}

	/**
	 * Sets the current generation of the optimization.
	 * 
	 * @param currentGeneration
	 *            new current generation
	 */
	public void setCurrentGeneration(int currentGeneration) {
		this.currentGeneration = currentGeneration;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see optimization.OptimizationI#getResults()
	 */
	@Override
	public ArrayList<double[]> getResults() {
		return result;
	}

	/**
	 * Sets the result value for the current generation.
	 * 
	 * @param value
	 *            value for the current generation
	 */
	public void setResult(double[] value) {
		result.add(value);
	}

	/**
	 * Returns the used random number generator.
	 * 
	 * @return the rng
	 */
	public RandomNumberGenerator getRng() {
		return rng;
	}

	/**
	 * Sets the random number generator for the optimization.
	 */
	public void setRng() {
		if (rng == null) {
			String temp = getProperties().getProperty("rng", "MersenneTwister");
			String seed = getProperties().getProperty("seed", "random");
			long seedSet;
			if (!seed.toLowerCase().equals("random")) {
				try {
					seedSet = Long.valueOf(seed);
				} catch (Exception e) {
					pushMessage("Unknown Seed in property file! Random seed set!");
					seedSet = Long.valueOf(System.currentTimeMillis());
				}
			} else {
				seedSet = System.currentTimeMillis();
			}
			if (temp.toLowerCase().equals("mersennetwister")) {
				rng = new MersenneTwisterFast(seedSet);
			} else if (temp.toLowerCase().equals("javarng")) {
				rng = new JavaRandomNumberGenerator(seedSet);
			} else {
				rng = new MersenneTwisterFast(seedSet);
			}
		}

	}

	/**
	 * Returns the fitness function of the optimization algorithm.
	 * 
	 * @return the fitnessFunction
	 */
	@SuppressWarnings("rawtypes")
	public SimpleFitnessFunction getFitness() {
		return fitnessFunction;
	}

	/**
	 * Sets the fitness function of the optimization algorithm.
	 * 
	 * @param fitness
	 *            new fitness function
	 */
	@SuppressWarnings("rawtypes")
	public void setFitness(SimpleFitnessFunction fitness) {
		if (changesAllowed()) {
			this.fitnessFunction = fitness;
			setStatus(NaturalOptimization.notInizialized);
			fitnessChanged = true;
		} else {
			pushError(new Exception(
					"Optimization running, changes not allowed!"));
		}
	}

	/**
	 * Returns the maximum calculation time. After reaching the time the
	 * optimization is stopped. If the value is 0, then there is no time limit.
	 * 
	 * @return the calculationTime
	 */
	public long getCalculationTime() {
		return Long.valueOf(properties.getProperty("calculationTime", "0"));
	}

	/**
	 * Returns the number of generations to optimize.
	 * 
	 * @return number of generations
	 */
	public int getGenerations() {
		return Integer.valueOf(properties.getProperty("generations", "100"));
	}

	/**
	 * Returns the search space of the optimization.
	 * 
	 * @return the searchSpace
	 */
	@SuppressWarnings("rawtypes")
	public SearchSpace getSearchSpace() {
		return searchSpace;
	}

	/**
	 * Sets the search space of the optimization.
	 * 
	 * @param searchSpace
	 *            new searchSpace
	 */
	@SuppressWarnings("rawtypes")
	public void setSearchSpace(SearchSpace searchSpace) {
		if (changesAllowed()) {
			this.searchSpace = searchSpace;
		} else {
			pushError(new Exception(
					"Optimization running, cahnges not allowed!"));
		}
	}

	/**
	 * Exports the individual data to file.
	 */
	public void exportIndividualData() {
		if (Boolean.valueOf(getProperties()
				.getProperty("indDataExport", "true"))) {
			try {
				if (this instanceof ParticleSwarmOptimization) {
					ParticleSwarmOptimization pso = (ParticleSwarmOptimization) this;
					IndividualDataExport.exportSwarmData(pso.getSwarm(),
							getCurrentGeneration());
				} else if (this instanceof EvolutionaryAlgorithm) {
					EvolutionaryAlgorithm ea = (EvolutionaryAlgorithm) this;
					IndividualDataExport.exportIndividualData(
							ea.getParentPopulation(), getCurrentGeneration());
				}
			} catch (Exception e) {
				pushError(e);
			}
		}
	}

	/**
	 * Sets the dimensionality of the optimization problem.
	 * 
	 * @param dim
	 *            new dimensionality
	 */
	public void setDimensionality(int dim) {
		if (changesAllowed()) {
			if (dim < 1) {
				pushError(new IllegalArgumentException(
						"Dimensionality must be greater than 0"));
			} else {
				properties.setProperty("dimension", dim + "");
				searchSpace.setDimensionality(dim);
			}
		} else {
			pushError(new Exception(
					"Optimization running, cahnges not allowed!"));
		}
	}

	/**
	 * Returns the dimensionality of the optimization problem.
	 * 
	 * @return dimensionality of the optimization problem
	 */
	public int getDimensionality() {
		return Integer.valueOf(properties.getProperty("dimension", "10"));
	}

	/**
	 * Sets the boundaries in search space.
	 * 
	 * @param boundaries
	 */
	@SuppressWarnings("unchecked")
	public void setBoundaries(Object[] boundaries) {
		if (changesAllowed()) {
			searchSpace.setBoundaries(boundaries);
		} else {
			pushError(new Exception(
					"Optimization running, cahnges not allowed!"));
		}
	}

	/**
	 * Returns the boundaries of the optimization problem.
	 * 
	 * @return the boundaries of the optimization problem
	 */
	public Object getBoundaries() {
		return searchSpace.getBoundaries();
	}

	/**
	 * Adds the given termination criterion to the list of criterions.
	 * 
	 * @param term
	 *            new terination criterion
	 */
	public void addTerminationCriterion(TerminationCriterion term) {
		if (changesAllowed()) {
			stopCriterions.add(term);
		} else {
			pushError(new Exception(
					"Optimization running, cahnges not allowed!"));
		}
	}

	/**
	 * Returns the change interval for the {@link NoChangeTerminationCriterion}
	 * to check.
	 * 
	 * @return change interval
	 */
	public int getChangeInterval() {
		return Integer.valueOf(getProperties().getProperty("changeInterval",
				"10"));
	}

	/**
	 * Returns the change percentage for the
	 * {@link NoChangeTerminationCriterion} to check.
	 * 
	 * @return change percentage
	 */
	public double getChangePercentage() {
		return Double.valueOf(getProperties().getProperty("changePercentage",
				"0.01"));
	}

	/**
	 * Returns the change percentage for the
	 * {@link NoChangeTerminationCriterion} to check.
	 * 
	 * @return change percentage
	 */
	public double getStandardDeviationLimit() {
		return Double.valueOf(getProperties().getProperty("stdLimit", "0.5"));
	}

	/**
	 * Returns the change percentage for the
	 * {@link NoChangeTerminationCriterion} to check.
	 * 
	 * @return change percentage
	 */
	public boolean getGenerationLimit() {
		return Boolean.valueOf(getProperties().getProperty("generationLimit",
				"true"));
	}

	// //////////////////////////////////////////////////////////////////////
	// //////////////////////////////////////////////////////////////////////
	// ///////////////////////Observer messages//////////////////////////////
	// //////////////////////////////////////////////////////////////////////
	// //////////////////////////////////////////////////////////////////////
	/**
	 * Propagates the Exception to all observers.
	 * 
	 * @param e
	 *            exception to propagate
	 */
	public void pushError(Exception e) {
		setChanged();
		notifyObservers(e);
		if (Boolean.valueOf(properties.getProperty("errorExport", "true"))) {
			ErrorLog.Error(e);
		}
	}

	/**
	 * Propagates the message to all observers.
	 * 
	 * @param e
	 *            message to propagate
	 */
	public void pushMessage(String e) {
		setChanged();
		notifyObservers(e);
	}

	/**
	 * Returns the properties object.
	 * 
	 * @return properties
	 */
	public JOFProperties getProperties() {
		return properties;
	}

}
